﻿
namespace Patterns4Net.ArchitectureExplorer.ViewModels.Graph
{
    using Infrastructure;
    using System.Collections.Generic;
    using System.ComponentModel.Composition;
    using System.Linq;
    using Caliburn.Micro;
    using Diagram;
    using Attributes;
    using Core;
    using Graphviz4Net.Graphs;
    using Core.AbstractionLevel;

    /// <summary>
    /// Provides the default implementation of <see cref="IGraphViewModelFactory"/>.
    /// </summary>
    [Export(typeof(IGraphViewModelFactory))]
    public class GraphViewModelFactory : IGraphViewModelFactory
    {
        private readonly IEventAggregator eventAggregator;

        private readonly Settings settings;

        [ImportingConstructor]
        public GraphViewModelFactory(IEventAggregator eventAggregator, Settings settings)
        {
            this.eventAggregator = eventAggregator;
            this.settings = settings;
        }

        /// <summary>
        /// Creates graph for types in the package.
        /// </summary>
        /// <remarks>
        /// <para>
        /// From the specification: classes from the package are displayed.
        /// If the package contains at least on class of level <see cref="AbstractionLevel.Higher"/>, then classes of
        /// levels <see cref="AbstractionLevel.Higher"/> and <see cref="AbstractionLevel.Normal"/> are displayed.
        /// If the package does not contain any class of level <see cref="AbstractionLevel.Higher"/>, then classes of
        /// levels <see cref="AbstractionLevel.Normal"/> and <see cref="AbstractionLevel.Low"/> are displayed.
        /// </para>
        /// </remarks>
        public GraphViewModel CreateGraphForPackage(DiagramViewModel diagramViewModel, Package package)
        {
            var diagram = diagramViewModel.Diagram;
            var result = new GraphViewModel(AbstractionLevel.Normal, this.settings);
            var vertices = new Dictionary<DiagramElement, VertexViewModel>();
            var level = AbstractionLevel.Low;
            if (package.Types.Any(t => t.AbstractionLevel == AbstractionLevel.Higher))
            {
                level = AbstractionLevel.Normal;
            }

            var types = package.Types.Where(t => t.AbstractionLevel.IsHigherOrEqual(level));
            var packageSubGraph = new SubGraphViewModel(package, this.eventAggregator);
            result.AddSubGraph(packageSubGraph);
            foreach (var typeInfo in types)
            {
                var vertex = new TypeInfoViewModel(result, typeInfo, this.eventAggregator);
                packageSubGraph.AddVertex(vertex);
                vertices.Add(typeInfo, vertex);
            }

            var relations =
                diagram.Relations.Where(r => types.Contains(r.Source) && types.Contains(r.Target));
            this.MergeRelations(relations, vertices).ForEach(result.AddEdge);

            return result;
        }

        /// <summary>
        /// Creates graph with packages in the layer and with some type in these packages.
        /// </summary>
        /// <remarks>
        /// <para>
        /// From the specification: Layer level: if the layer contains at least one 
        /// class of level <see cref="AbstractionLevel.Higher"/>, then
        /// these classes are displayed. Otherwise <see cref="AbstractionLevel.Normal"/> 
        /// level classes from the layer are displayed. Classes have labels with their names, 
        /// but members are not displayed. 
        /// Packages are displayed as rectangles and contain corresponding classes.
        /// </para>
        /// </remarks>
        public GraphViewModel CreateGraphForLayer(DiagramViewModel diagramViewModel, Layer layer)
        {
            var diagram = diagramViewModel.Diagram;            
            AbstractionLevel level = AbstractionLevel.Normal;
            if (layer.Packages.SelectMany(p => p.Types).Count(t => t.AbstractionLevel == AbstractionLevel.Higher) > layer.Packages.Count())
            {
                level = AbstractionLevel.Higher;
            }

            var result = new GraphViewModel(level, this.settings);
            var subGraphsMap = new Dictionary<DiagramElement, SubGraphViewModel>();
            foreach (var package in layer.Packages)
            {
                var subGraph = new SubGraphViewModel(package, this.eventAggregator);
                subGraphsMap.Add(package, subGraph);
                result.AddSubGraph(subGraph);
                var vertices = new Dictionary<DiagramElement, VertexViewModel>();
                var packageTypes = package.Types.Where(t => t.AbstractionLevel.IsHigherOrEqual(level));
                foreach (var type in packageTypes)
                {
                    var vertex = new TypeInfoViewModel(result, type, this.eventAggregator);
                    subGraph.AddVertex(vertex);
                    vertices.Add(type, vertex);
                }

                // empty packages are messed up by Graphviz, so we put at least a dummy vertex in it.
                if (packageTypes.IsEmpty())
                {
                    var vertex = new DummyVertexViewModel(package, this.eventAggregator);
                    subGraph.AddVertex(vertex);
                }

                var relations =
                    diagram.Relations.Where(r => packageTypes.Contains(r.Source) && packageTypes.Contains(r.Target));
                this.MergeRelations(relations, vertices).ForEach(result.AddEdge);
            }

            var packagesRelations = this.CreateRelationsBetweenPackages(layer.Packages, diagram);
            this.MergeRelations(packagesRelations, subGraphsMap).ForEach(result.AddEdge);
            return result;
        }

        public GraphViewModel CreateGraphForType(DiagramViewModel diagramViewModel, TypeInfo mainType)
        {
            var diagram = diagramViewModel.Diagram;
            var relations = diagram.Relations.Where(r => r.Source == mainType || r.Target == mainType);
            var types = relations.Select(r => r.Source == mainType ? r.Target : r.Source).Union(new[] { mainType }).Cast<TypeInfo>();
            var result = new GraphViewModel(AbstractionLevel.Low, this.settings);
            var packages = types.Select(v => (Package)v.Parent).Distinct();
            var vertices = new Dictionary<DiagramElement, VertexViewModel>();

            // add relations between selected types that should always be visible
            var additionalRelations = from r in diagram.Relations
                                      where this.settings.ShowAlwaysRelations.Contains(r.GetType())
                                      where types.Contains(r.Source) && types.Contains(r.Target)
                                      select r;
            relations = relations.Union(additionalRelations);

            foreach (var package in packages)
            {
                var subGraph = new SubGraphViewModel(package, this.eventAggregator);
                result.AddSubGraph(subGraph);
                foreach (var type in types.Where(t => t.Parent == package))
                {
                    var vertex = new TypeInfoViewModel(result, type, this.eventAggregator);
                    if (type == mainType)
                    {
                        vertex.ShowExpanded = true;
                    }

                    subGraph.AddVertex(vertex);
                    vertices.Add(type, vertex);
                }
            }

            this.MergeRelations(relations, vertices).ForEach(result.AddEdge);
            return result;
        }

        public GraphViewModel CreateGraphForDiagram(DiagramViewModel diagramViewModel, Diagram diagram)
        {
            var result = new GraphViewModel(AbstractionLevel.Higher, this.settings);
            var vertices = new Dictionary<DiagramElement, SubGraphViewModel>();

            // Create Subgraphs
            foreach (var layer in diagram.Layers)
            {
                var subGraph = new SubGraphViewModel(layer, this.eventAggregator);
                result.AddSubGraph(subGraph);
                this.AddToGraph(
                    layer.Packages, 
                    CreateRelationsBetweenPackages(layer.Packages, diagram).ToArray(), 
                    subGraph,
                    result);
                vertices.Add(layer, subGraph);
            }

            var relations = this.CreateRelationsBetweenLayers(diagram);
            this.MergeRelations(relations, vertices).ForEach(result.AddEdge);
            return result;
        }


        /// <summary>
        /// Adds elements as <see cref="VertexViewModel"/> instances.
        /// </summary>
        private void AddToGraph(
            IEnumerable<DiagramElement> elements, 
            IEnumerable<Relation> relations, 
            IVerticesCollection<VertexViewModel> graph, 
            GraphViewModel mainGraph)
        {
            var map = new Dictionary<DiagramElement, VertexViewModel>();
            foreach (var element in elements)
            {
                var viewModel = new VertexViewModel(element, this.eventAggregator);
                graph.AddVertex(viewModel);
                map.Add(element, viewModel);
            }

            this.MergeRelations(relations, map).ForEach(mainGraph.AddEdge);
        }

        private IEnumerable<Relation> CreateRelationsBetweenPackages(IEnumerable<Package> packages, Diagram diagram)
        {
            var result = 
                   (from relation in diagram.Relations
                   where relation.Source.Parent != relation.Target.Parent
                   where packages.Contains(relation.Source.Parent)
                   where packages.Contains(relation.Target.Parent)
                   select relation.Create(relation.Source.Parent, relation.Target.Parent)).ToArray();
            result.ForEach(r => r.Name = string.Empty);
            result.ForEach(r => r.HasCardinalities = false);
            return result.Distinct((r1, r2) => r1.Equals(r2));
        }

        private IEnumerable<Relation> CreateRelationsBetweenLayers(Diagram diagram)
        {
            var result = 
                   (from relation in diagram.Relations
                   where relation.Source.Parent.Parent != relation.Target.Parent.Parent
                   select relation.Create(relation.Source.Parent.Parent, relation.Target.Parent.Parent)).ToArray();
            result.ForEach(r => r.Name = string.Empty);
            result.ForEach(r => r.HasCardinalities = false);
            return result.Distinct((r1, r2) => r1.Equals(r2));
        }

        private IEnumerable<RelationViewModel<TViewModel>> MergeRelations<TViewModel>(
            IEnumerable<Relation> relations, 
            IDictionary<DiagramElement, TViewModel> vertices)
        {
            return from relation in ArchitectureExplorer.Diagram.Services.MergeRelations.Merge(relations)
                   select new RelationViewModel<TViewModel>(vertices[relation.Source], vertices[relation.Target], relation);
        }
    }
}
